package clustering;

import engine.AbstractVectorTI;
import engine.Tweet;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.TreeMap;
import javax.swing.SwingWorker;
import ui.UserInterface;

//Algorithme des k-moyennes
public class AlgoKMeans extends SwingWorker<KMeansResult, Integer> {

    public static enum Distance {

        Manhattan, Euclidienne, Minkowski
    }
    private Tweet[] tweets;
    private Integer clusterCount;
    private Distance distanceType;
    private Integer minkowskiParameter;
    private Integer nbIterations;
    private Double tresholdDistance;
    private AbstractVectorTI[] centroids;
    private UserInterface gui;//pour update la progression

    public AlgoKMeans(Tweet[] tweets,
            Integer clusterCount,
            Distance distanceType,
            Integer minkowskiParameter,
            Integer nbIterations,
            Double tresholdDistance,
            AbstractVectorTI[] centroids) {

        this.tweets = tweets;
        this.clusterCount = clusterCount;
        this.distanceType = distanceType;
        this.minkowskiParameter = minkowskiParameter;
        this.nbIterations = nbIterations;
        this.tresholdDistance = tresholdDistance;
        this.centroids = centroids;
    }

    public void setGui(UserInterface gui) {
        this.gui = gui;
    }

    //Retourne la distance entre deux vecteurs
    //0 si un des deux vecteur est null
    public double distance(AbstractVectorTI v1, AbstractVectorTI v2) {
        if (v1 == null || v2 == null) {
            return 0d;
        }

        if (distanceType == Distance.Manhattan) {
            return v1.distanceManhattan(v1, v2);
        } else if (distanceType == Distance.Minkowski) {
            return v1.distanceMinkowski(v1, v2, minkowskiParameter);
        } else {//Par defaut
            return v1.distanceEuclidienne(v1, v2);
        }
    }

    @Override
    protected KMeansResult doInBackground() throws Exception {
        assert (clusterCount > 0);

        //Détermine condition d'arret
        Boolean stopCondIsIteration = (tresholdDistance == null);
        Boolean algoFinished;

        //map qui associe à chaque centroide la liste des vecteurs les plus proches
        //la clé est l'index du centroides dans le tableau "centroids"
        //la valeur est la liste des vecteurs
        TreeMap<Integer, ArrayList<AbstractVectorTI>> mapCentroidToVectors;
        ArrayList<AbstractVectorTI> currentCentroidVectors;

        //Centroides
        if (this.centroids == null) {
            centroids = new AbstractVectorTI[clusterCount];

            //Création de centroides placés aléatoirement sur des vecteurs existants
            Random rand = new Random();
            Integer randomVectorIndex;
            for (int i = 0; i < clusterCount; i++) {
                randomVectorIndex = rand.nextInt(tweets.length);//indice aléatoire d'un vecteur de "tweets"

                //Copie du vecteur (!)
                centroids[i] = tweets[randomVectorIndex].getVector().clone();
                //System.out.println(i + "  " + centroids[i]);
            }
        }
        AbstractVectorTI[] newCentroids;

        Double distNearestCentroid, currentDistance;
        Integer nearestCentroidIndex;
        AbstractVectorTI currentVector;
        AbstractVectorTI currentAverage;

        //Pour la progression
        double currenDist;
        double distMax = -1;
        double distMaxInitial = 0;
        boolean distAffected = false;
        Integer iterationCount = 0;
        Integer progressionCur = 0, progressionNew;
        Integer progressCalcDist, previousProgressCalcDist;


        do {
            mapCentroidToVectors = new TreeMap<Integer, ArrayList<AbstractVectorTI>>();

            previousProgressCalcDist = 0;

            System.out.println("Calcul distances...");
            //Parcours de chaque vecteur afin de déterminer son centroide le plus proche
            for (int i = 0; i < tweets.length; i++) {

                //Copie du vecteur
                currentVector = tweets[i].getVector().clone();

                distNearestCentroid = Double.MAX_VALUE;
                nearestCentroidIndex = -1;//Doit forcement etre ecrase

                //calcul de la distance du vecteur courant avec chaque centroide
                for (int j = 0; j < centroids.length; j++) {
                    currentDistance = distance(centroids[j], currentVector);

                    if (currentDistance < distNearestCentroid) {
                        distNearestCentroid = currentDistance;
                        nearestCentroidIndex = j;
                    }
                }

                //le centroid le plus proche a été trouvé, on ajoute ce vecteur à ce centroid
                currentCentroidVectors = (mapCentroidToVectors.containsKey(nearestCentroidIndex)) ? mapCentroidToVectors.get(nearestCentroidIndex) : new ArrayList<AbstractVectorTI>();
                currentCentroidVectors.add(currentVector);
                mapCentroidToVectors.put(nearestCentroidIndex, currentCentroidVectors);

                progressCalcDist = 100 * i / tweets.length;
                if (progressCalcDist >= previousProgressCalcDist + 10) {
                    previousProgressCalcDist = progressCalcDist;
                    System.out.print(progressCalcDist + "%, ");
                }
            }
            System.out.println("...done");

            //On affiche le nombre de vecteurs associés à chaque cluster
            for (Map.Entry<Integer, ArrayList<AbstractVectorTI>> e : mapCentroidToVectors.entrySet()) {
                System.out.println("k" + e.getKey() + " : " + e.getValue().size());
            }

            System.out.println("Calcul positions centroides...");
            //On repositionne les centroides comme la moyenne de leurs vecteurs
            newCentroids = new AbstractVectorTI[clusterCount];
            for (int i = 0; i < newCentroids.length; i++) {
                if (mapCentroidToVectors.containsKey(i)) {
                    currentAverage = centroids[0].average(mapCentroidToVectors.get(i));
                } else {//Si le centroide n'a aucun vecteur, on ne le déplace pas, valeur particulière null
                    currentAverage = null;
                }

                newCentroids[i] = currentAverage;
            }

            System.out.println("...done.");

            System.out.println("Mesure distance max...");
            //On mesure la distance parcourue par chaque centroide pour déterminer celui qui s'est le plus déplacé
            distMax = Double.MIN_VALUE;
            for (int i = 0; i < newCentroids.length; i++) {
                currenDist = distance(newCentroids[i], centroids[i]);
                //System.out.println("dist i = " + i + "  " + currenDist);
                if (currenDist > distMax) {
                    distMax = currenDist;
                }
            }

            //On mémorise la première "distanceMax" (pour la progression)
            if (!distAffected) {
                distMaxInitial = distMax;
                distAffected = true;
                System.out.println("distMaxInitial = " + distMaxInitial);
            }

            System.out.println("...done.");

            System.out.println("Affectation centroides...");
            //Affectation des nouveaux centroides (si null, alors il n'y a pas eu de mouvement)
            for (int i = 0; i < newCentroids.length; i++) {
                if (newCentroids[i] != null) {
                    centroids[i] = newCentroids[i].clone();
                }
            }
            System.out.println("...done.");

            iterationCount++;

            //Affiche la plus grande distance parcourue
            System.out.println("distMax = " + distMax + "; (limite:" + tresholdDistance + ")");

            //Condition d'arret
            if (!stopCondIsIteration) {
                //Arret si la plus grde distance parcourue par un centroide est inférieur ou égale à la limite
                algoFinished = (distMax <= tresholdDistance);

                //Calcul progression
                progressionNew = (int) (100 * (1.0 - distMax / distMaxInitial));
                if (progressionNew > progressionCur) {
                    progressionCur = progressionNew;
                }
            } else {
                //Arret si le nombre d'iteration atteint celui demandé
                //System.out.println("iterations=" + iterationCount + "/" + this.nbIterations);
                algoFinished = iterationCount >= this.nbIterations;

                //Calcul progression
                progressionCur = 100 * iterationCount / this.nbIterations;
            }

            //Affichage de la progression
            //System.out.println("progression=" + progressionCur + "%");
            publish(progressionCur);

        } while (!algoFinished);

        //Fin algo

        //Création de l'objet résultat
        KMeansResult result = new KMeansResult();
        result.centroids = centroids;
        result.mapCentroidToVectors = mapCentroidToVectors;

        publish(100);
        return result;
    }

    @Override
    protected void process(List<Integer> values) {
        for (Integer val : values) {
            gui.updateBarKMeans(val);
        }
    }

    @Override
    protected void done() {
        gui.kMeansFinished();
    }
}
